package com.github.facadefs.oss;

import com.github.facadefs.FileView;
import com.github.facadefs.util.Operation;
import com.github.facadefs.util.Throwables;
import com.google.common.collect.Lists;
import com.qiniu.common.QiniuException;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.FileInfo;
import com.qiniu.storage.model.FileListing;
import com.qiniu.util.Auth;
import com.github.facadefs.Operations;
import com.github.facadefs.Selector;
import com.github.facadefs.util.Paths;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.List;

//import com.qiniu.common.Config;

/**
 *
 */
public class QiNiuOssFileSystem implements OssFileSystem {
    private static final int DEFAULT_PERMISSIONS = 0600;

    /**
     * 这个实际应该是 bucket 绑定的域名.
     */
    private final String endpoint;
    private final String accessKey;
    private final String secretKey;
    private final String bucket;

    private final String chroot;

    public QiNiuOssFileSystem(final String endpoint, final String accessKey,
                              final String secretKey, final String bucket) {
        this(endpoint, accessKey, secretKey, bucket, null);
    }

    public QiNiuOssFileSystem(final String endpoint, final String accessKey,
                              final String secretKey, final String bucket, final String chroot) {
        this.endpoint = endpoint;
        this.accessKey = accessKey;
        this.secretKey = secretKey;
        this.bucket = bucket;
        this.chroot = chroot;
    }

    @Override
    public boolean exists(String path) throws IOException {
        return null != stat(path);
    }

    @Override
    public FileView stat(String path) throws IOException {
        final String key = toAbsoluteKey(path);
        final String bucket = getBucket();

        final BucketManager oss = client();
        if (isRootKey(key)) {
            return new FileView("", 0, true, 0, 0, DEFAULT_PERMISSIONS, null, null, null);
        }

        final FileInfo metadata = stat1(oss, bucket, key);
        return null != metadata ? toView(bucket, key, metadata) : null;
    }

    private FileInfo stat0(final BucketManager oss, final String bucket, final String key) {
        try {
            final FileInfo stat = oss.stat(bucket, key);
            if (null == stat.key) {
                stat.key = key;
            }
            return stat;
        } catch (final QiniuException e) {
            return null;
        }
    }

    private FileInfo stat1(final BucketManager oss, final String bucket, final String key) throws QiniuException {
        final FileInfo stat = stat0(oss, bucket, key);
        if (null != stat) {
            return stat;
        }

        // try as directory
        String directoryKey = key;
        if (isObjectKey(key)) {
            directoryKey = toDirectoryKey(key);
            final FileInfo directoryStat = stat0(oss, bucket, directoryKey);
            if (null != directoryStat) {
                return directoryStat;
            }
        }

        final FileListing listing = oss.listFilesV2(bucket, directoryKey, null, 1, "/");
        final FileInfo[] items = cleanup(listing.items, directoryKey);
        if (items.length > 0 || listing.commonPrefixes.length > 0) {
            final FileInfo info = new FileInfo();
            info.key = directoryKey;
            info.fsize = 0;
            info.putTime = -1;
            return info;
        }
        return null;
    }

    @Override
    public FileView[] ls(final String path) throws IOException {
        final List<FileView> views = Lists.newLinkedList();
        ls(path, new Selector() {
            @Override
            public int select(final FileView view) {
                views.add(view);
                return CONTINUE;
            }
        });
        return views.toArray(new FileView[0]);
    }

    @Override
    public void ls(final String path, final Selector selector) throws IOException {
        final String key = toAbsoluteKey(path);
        final String bucket = getBucket();

        final BucketManager oss = client();
        if (isObjectKey(key)) {
            final FileInfo stat = stat0(oss, bucket, key);
            if (null != stat) {
                selector.select(toView(bucket, key, stat));
                return;
            }
        }

        // root or as directory.
        final String prefix = !isRootKey(key) ? toDirectoryKey(key) : null;
        String marker = null;
        do {
            final FileListing listing = oss.listFilesV2(bucket, prefix, marker, 100, "/");
            final FileInfo[] items = cleanup(listing.items, prefix);
            for (final FileInfo object : items) {
                if (Selector.CONTINUE != selector.select(toView(bucket, null, object))) {
                    break;
                }
            }

            for (final String directoryKey : listing.commonPrefixes) {
                if (Selector.CONTINUE != selector.select(toView(bucket, directoryKey))) {
                    break;
                }
            }
            marker = listing.marker;
        } while (null != marker && !marker.isEmpty());
    }

    @Override
    public boolean mkdirs(String path) throws IOException {
        final String key = toAbsoluteKey(path);
        final String bucket = getBucket();
        try {
            final String directoryKey = toDirectoryKey(key);
            putBytes(bucket, directoryKey, new byte[0]);
            return true;
        } catch (final Exception ex) {
            return Throwables.rethrowIOException(ex);
        }
    }

    @Override
    public void rm(final String path, final boolean recursive) throws IOException {
        final String key = toAbsoluteKey(path);
        final String bucket = getBucket();

        try {
            final BucketManager oss = client();
            if (isObjectKey(key) && null != stat0(oss, bucket, key)) {
                oss.delete(bucket, key);
                return;
            }

            // 删除失败或不是文件
            final String prefix = !isRootKey(key) ? toDirectoryKey(key) : null;

            String marker = null;
            do {
                final FileListing listing = oss.listFilesV2(bucket, prefix, marker, 100, "/");
                // final List<String> directories = listing.commonPrefixes;
                if (listing.items.length > 0) {
                    if (!recursive) {
                        throw new IOException("You cannot delete non-empty directory, use recurse=true to overide");
                    }

                    for (final FileInfo object : listing.items) {
                        oss.delete(bucket, object.key);
                    }
                }
                marker = listing.marker;
            } while (null != marker && !marker.isEmpty());

            if (null != prefix && null != stat0(oss, bucket, prefix)) {
                oss.delete(bucket, prefix);
            }
        } catch (final Exception ex) {
            Throwables.rethrowIOException(ex);
        }
    }

    @Override
    public void rename(String oldPath, String newPath) throws IOException {
        String bucket = getBucket();
        String fKey = toAbsoluteKey(oldPath);
        String tKey = toAbsoluteKey(newPath);
        try {
            // FIXME
            // client().move(bucket, fKey, bucket, tKey);
            client().rename(bucket, fKey, tKey);
        } catch (QiniuException e) {
            throw new IOException(e);
        }
    }

    @Override
    public InputStream open(final String path) throws IOException {
        final String key = toAbsoluteKey(path);
        final String bucket = getBucket();
        try {
            final BucketManager oss = client();
            final FileInfo stat = stat1(oss, bucket, key);
            if (null == stat) {
                return null;
            } else if (isDirectoryKey(stat.key)) {
                throw new IOException("Can't open " + path + " because it is a directory");
            }

            String url;
            if (endpoint.endsWith(Paths.PATH_SEPARATOR)) {
                url = endpoint + key;
            } else {
                url = endpoint + Paths.PATH_SEPARATOR + key;
            }
            final String downloadUrl = getAuth().privateDownloadUrl(url);
            return new URL(downloadUrl).openStream();
        } catch (final Exception ex) {
            return Throwables.rethrowIOException(ex);
        }
    }

    @Override
    public OutputStream create(final String path, final boolean override) throws IOException {
        return create(path, null, override);
    }

    @Override
    public OutputStream create(final String path, final String contentType, final boolean override) throws IOException {
        final String key = toAbsoluteKey(path);
        final String bucket = getBucket();

        try {
            final BucketManager client = client();
            final FileInfo stat = stat1(client, bucket, key);
            if (null != stat) {
                if (!override) {
                    throw new IOException("File already exists: " + path);
                } else if (isDirectoryKey(stat.key)) {
                    throw new IOException("Can't override " + path + " because it is a directory");
                }
            }

            final File tempFile = File.createTempFile("qiniu.upload.", ".tmp");
            return new DataOutputStream(new FileOutputStream(tempFile)) {
                @Override
                public void close() throws IOException {
                    super.close();
                    try {
                        putFile(bucket, key, tempFile, contentType);
                    } finally {
                        if (!tempFile.delete()) {
                            tempFile.deleteOnExit();
                        }
                    }
                }
            };
        } catch (final Exception ex) {
            return Throwables.rethrowIOException(ex);
        }
    }

    @Override
    public <R> R execute(final Operation<Operations, R> operation) throws IOException, IOException {
        return operation.execute(this);
    }

    private void putBytes(final String bucket, final String key, final byte[] data) throws QiniuException {
        final String token = getAuth().uploadToken(bucket, key);
        final UploadManager um = new UploadManager(conf());
        um.put(data, key, token);
    }

    private void putFile(final String bucket, final String key, final File file, final String mime) throws QiniuException {
        final String token = getAuth().uploadToken(bucket, key);
        final UploadManager um = new UploadManager(conf());
        um.put(file, key, token, null, mime, false);
    }


    private String getBucket() {
        return bucket;
    }

    private BucketManager client() {
        return new BucketManager(getAuth(), conf());
    }

    private Auth getAuth() {
        return Auth.create(accessKey, secretKey);
    }

    private Configuration conf() {
        return new Configuration();
    }


    private boolean isRootKey(final String key) {
        return null == key || key.isEmpty();
    }

    private boolean isObjectKey(final String key) {
        return null != key && !key.isEmpty() && !key.endsWith(Paths.PATH_SEPARATOR);
    }

    private boolean isDirectoryKey(final String key) {
        return null != key && !key.isEmpty() && key.endsWith(Paths.PATH_SEPARATOR);
    }

    private String toDirectoryKey(final String key) {
        return null != key && !key.endsWith(Paths.PATH_SEPARATOR) ? key + Paths.PATH_SEPARATOR : key;
    }

    private String toAbsoluteKey(String path) {
        return toAbsoluteKey(chroot, path);
    }

    private String toAbsoluteKey(final String chroot, final String path) {
        if (null == path) {
            throw new IllegalArgumentException("path muse be not empty");
        }
        if (null != chroot && !chroot.isEmpty()) {
            return Paths.toAbsolutePath(chroot, path);
        } else if (path.startsWith(Paths.ROOT)) {
            return Paths.toRelativePath(path, Paths.ROOT);
        }
        return path;
    }

    private String toRelativeKey(final String absKey) {
        if (null != chroot && !chroot.isEmpty()) {
            return Paths.toRelativePath(chroot, absKey);
        }
        return absKey;
    }

    private FileView toView(final String bucket, final String key, final FileInfo metadata) {
        final String keyToUse = null != metadata.key ? metadata.key : key;
        final long length = metadata.fsize;
        long mtime = metadata.putTime;
        String objectType = metadata.mimeType;
        String owner = metadata.endUser;
        String group = owner;

        return new FileView(keyToUse, length, isDirectoryKey(key), mtime, mtime, DEFAULT_PERMISSIONS, owner, group, null);
    }

    private FileView toView(final String bucket, final String key) {
        return new FileView(toRelativeKey(key), -1, true, -1, -1, DEFAULT_PERMISSIONS, null, null, null);
    }

    private FileInfo[] cleanup(final FileInfo[] items, final String cleanKey) {
        final List<FileInfo> candidates = Lists.newArrayListWithExpectedSize(items.length);
        for (final FileInfo item : items) {
            if (!cleanKey.equals(item.key)) {
                candidates.add(item);
            }
        }
        return candidates.toArray(new FileInfo[0]);
    }
}
